/*
 * Copyright 2011 group coding kata nantes
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package fr.coding.kata.nantes.bench;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.Callable;

/**
 * Base class to extends by runners for specifics Kata to test.
 *
 * @param <E> Type of the objects to test.
 * @param <I> Type of the input data.
 * @param <R> Type of the result.
 * @author Sylvain Leduby
 * @since 1.0
 */
public abstract class AbstractRunner<E extends Callable<R>, I, R> {

    /**
     * Logger.
     */
    private static final Logger LOG = LoggerFactory.getLogger(AbstractRunner.class);

    private final Game<E, I, R> game;

    /**
     * Protected constructor.
     * <p/>
     * Runners should provide the targets to test and a collection of data (an input and the
     * corresponding expected result) to use in the tests.
     *
     * @param targets The instances to test.
     * @param data    A collection of data (couple of an input and the corresponding expected result)
     *                to use in the tests
     */
    protected AbstractRunner(final Collection<E> targets, final Collection<Couple<I, R>> data) {
        super();
        this.game = new Game<E, I, R>(targets, data);
    }

    public void benchmarkAll() {
        LOG.debug("Method start: benchmarkAll");

        final Collection<Couple<I, R>> data = game.getData();
        for (final Couple<I, R> couple : data) {
            benchmark(couple.getInput(), couple.getExpectResult());
        }

        LOG.debug("Method end: benchmarkAll");
    }

    /**
     * Main method to run a benchmark.
     *
     * @param data     The data to use for the test.
     * @param expected The expected result with given {@code data}.
     */
    public void benchmark(final I data, final R expected) {
        LOG.debug("Method start: benchmark({}, {})", data, expected);

        processResults(expected, runGames(data));
        processResultsWithInit(expected, runGamesWithInit(data));

        LOG.debug("Method end: benchmark");
    }

    /**
     * This method run every implementations with given input data.
     *
     * @param line The data to run the benchmark.
     * @return A collection of {@link Run} that represents the results of the tests.
     */
    private Collection<Run<E, R>> runGames(final I line) {
        LOG.debug("Method start: runGames");

        final Collection<E> targets = game.getTargets();

        // test each target
        final Collection<Run<E, R>> runs = new ArrayList<Run<E, R>>(targets.size());
        for (final E target : targets) {
            runs.add(runGame(new Run<E, R>(target), line));
        }

        LOG.debug("Method end: runGames");

        return runs;
    }

    /**
     * This method run every implementations with given input data. duration with init.
     *
     * @param line The data to run the benchmark.
     * @return A collection of {@link Run} that represents the results of the tests.
     */
    private Collection<Run<E, R>> runGamesWithInit(final I line) {
        LOG.debug("Method start: runGamesWithInit");

        final Collection<E> targets = game.getTargets();

        // test each target
        final Collection<Run<E, R>> runs = new ArrayList<Run<E, R>>(targets.size());
        for (final E target : targets) {
            runs.add(runGameWithInit(new Run<E, R>(target), line));
        }

        LOG.debug("Method end: runGamesWithInit");

        return runs;
    }

    /**
     * This method runs a game within given implementation. duration without init.
     *
     * @param run   The implementation to run.
     * @param input The data to use.
     * @return The result of the run.
     */
    private Run<E, R> runGame(final Run<E, R> run, final I input) {
        LOG.debug("Benchmarking '{}', with '{}'", run.getName(), input);

        try {
            // prepare the target to run (set input data)
            final E runReady = init(run.getTarget(), input);
            final long start = System.nanoTime();
            run.setResult(runReady.call());
            run.setDuration(System.nanoTime() - start);

        } catch (final Exception e) {
            LOG.error("An error occurs when running a test.", e);
        }

        return run;
    }

    /**
     * This method runs a game within given implementation. duration with init.
     *
     * @param run   The implementation to run.
     * @param input The data to use.
     * @return The result of the run.
     */
    private Run<E, R> runGameWithInit(final Run<E, R> run, final I input) {
        LOG.debug("Benchmarking '{}', with '{}'", run.getName(), input);

        try {
            // prepare the target to run (set input data)
            final long start = System.nanoTime();
            final E runReady = init(run.getTarget(), input);
            run.setResult(runReady.call());
            run.setDuration(System.nanoTime() - start);

        } catch (final Exception e) {
            LOG.error("An error occurs when running a test.", e);
        }

        return run;
    }

    /**
     * This method checks if the results are OK and search for the fastest implementation.
     *
     * @param expected The expected result.
     * @param runs     The runs.
     */
    private void processResults(final R expected, final Collection<Run<E, R>> runs) {
        LOG.debug("Analyzing results");

        final StringBuilder failures = new StringBuilder();
        Run<E, R> fastest = new Run<E, R>(null, null, Long.MAX_VALUE);
        for (final Run<E, R> run : runs) {
            // Check the result is OK
            if (passed(expected, run.getResult())) {
                LOG.info("An implementation passed the test: {} in {}ns", run.getName(), run.getDuration());

                // did it run faster than the other?
                if (fastest.getDuration().compareTo(run.getDuration()) > 0) {
                    fastest = run;
                }
            } else {
                LOG.info("An implementation did not pass the test: {}", run.getName());
                failures.append(' ').append(run.getName());
            }
        }

        if (fastest.getTarget() != null) {
            LOG.info("The winner is: {} in {}ns", fastest.getName(), fastest.getDuration());
        }

        if (failures.length() != 0) {
            LOG.warn("Some implementation(s) did not pass the test: {}", failures.toString());
        }
    }

    /**
     * This method checks if the results are OK and search for the fastest implementation. with init
     * duration.
     *
     * @param expected The expected result.
     * @param runs     The runs.
     */
    private void processResultsWithInit(final R expected, final Collection<Run<E, R>> runs) {
        LOG.debug("Analyzing results");

        final StringBuilder failures = new StringBuilder();
        Run<E, R> fastest = new Run<E, R>(null, null, Long.MAX_VALUE);
        for (final Run<E, R> run : runs) {
            // Check the result is OK
            if (passed(expected, run.getResult())) {
                LOG.info("An implementation passed the test (with init): {} in {}ns", run.getName(),
                        run.getDuration());

                // did it run faster than the other?
                if (fastest.getDuration().compareTo(run.getDuration()) > 0) {
                    fastest = run;
                }
            } else {
                LOG.info("An implementation did not pass the test (with init): {}", run.getName());
                failures.append(' ').append(run.getName());
            }
        }

        if (fastest.getTarget() != null) {
            LOG.info("The winner (with init) is: {} in {}ns", fastest.getName(), fastest.getDuration());
        }

        if (failures.length() != 0) {
            LOG.warn("Some implementation(s) did not pass the test: {}", failures.toString());
        }
    }

    /**
     * This method compares the expected result with the obtained result and tells if the test is
     * OK.
     * <p/>
     * Subclass can override the method to do specific comparison on the values.
     *
     * @param expected The expected result.
     * @param result   The obtained result.
     * @return <code>true</code> if both values are <code>null</code> or equals.
     */
    protected boolean passed(final R expected, final R result) {
        return expected == null && result == null || expected != null && expected.equals(result);
    }

    /*
     * Abstract methods
     */

    /**
     * Sub classes should implement this method to provide initialization behavior (this method call
     * is included in the duration of the run).
     *
     * @param target The implementation that is tested.
     * @param input  The data to process.
     * @return The running implementation.
     */
    public abstract E init(E target, I input);

}
